Skip to content

feat(sdk-python): accept scope kwargs on memory set/get/delete and reconcile memory.search#707

Draft
santoshkumarradha wants to merge 3 commits into
mainfrom
feat/memory-scope-dx
Draft

feat(sdk-python): accept scope kwargs on memory set/get/delete and reconcile memory.search#707
santoshkumarradha wants to merge 3 commits into
mainfrom
feat/memory-scope-dx

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Jul 2, 2026

Copy link
Copy Markdown
Member

Fixes #712

Motivation

During tutorial writing, four independent LLM writers all guessed the same non-existent API:

app.memory.set(key, data, scope="global")

That signature does not exist. The Python MemoryInterface only exposed scoped access through accessors (app.memory.global_scope.set(...), app.memory.session(id).set(...), etc.), while the TypeScript SDK already accepts scope (and scopeId) positionally on set/get/delete. When four writers and four models independently reach for the same shape, the API should match the guess. This PR makes the Python API match both the natural guess and the TS SDK.

What changed

Two changes, nothing else.

1. Scope kwargs on MemoryInterface.set / get / delete

  • set, get, and delete now accept optional scope and scope_id keyword arguments.
  • scope=None (default) keeps today's behavior exactly: set uses automatic context scoping, get performs hierarchical lookup (workflow -> session -> actor -> global), delete uses the current scope. Zero behavior change for existing code.
  • scope="global" delegates to global_scope; scope="session"|"actor"|"workflow" delegates to the corresponding accessor and requires scope_id.
  • Invalid scope values raise ValueError listing the valid scopes. A non-global scope without a scope_id raises a ValueError that points the caller at the accessor form.
  • The accessor API is untouched and remains primary in the docstrings. The kwargs are a thin convenience layer that reuses the same scoped clients (GlobalMemoryClient / ScopedMemoryClient) via a single private dispatch helper, so there is one code path.

2. Reconcile memory.search

The README advertised app.memory.search(...), which does not exist. Corrected to the real method and fixed the scope names.

What was verified against the control plane

  • Memory routes: control-plane/internal/server/routes_memory.go:31-41 registers /memory/set, /memory/get, /memory/delete, /memory/list, and vector routes. The only search endpoint is POST /memory/vector/search (routes_memory.go:40). There is no general /memory/search.
  • API catalog confirms the same surface: control-plane/internal/server/apicatalog/catalog_entries.go:96-107 lists set/get/delete/list plus vector/search ("Similarity search over vectors"), and no plain memory search.
  • Canonical scope names are workflow, session, actor, globalcontrol-plane/internal/handlers/memory.go:273 (scopes := []string{"workflow", "session", "actor", "global"}) and the getScopeID switch at control-plane/internal/handlers/memory.go:422-433. There is no agent or run scope. The server derives scope_id from the X-Workflow-ID / X-Session-ID / X-Actor-ID headers (resolveScope, memory.go:402-418), which the Python MemoryClient._build_headers already sets from scope_id.
  • TS parity: sdk/typescript/src/memory/MemoryInterface.ts:41-117set/get/delete/exists/listKeys all take scope and scopeId positionally, and MemoryScope is 'workflow' | 'session' | 'actor' | 'global' (sdk/typescript/src/types/agent.ts:72).

Change 2 outcome: the server endpoint does NOT exist. Per scope, I did not invent a search method. Instead I fixed the README:

  • app.memory.set() / .get() / .search() -> .similarity_search() (the real method on MemoryInterface).
  • Vector-search table row app.memory.search(embedding, top_k=5) -> app.memory.similarity_search(embedding, top_k=5).
  • Four-scopes row Global, agent, session, run -> Global, actor, session, workflow (the actual scope names).

The low-level Python MemoryClient already implements similarity_search against /memory/vector/search, so no new SDK method was needed.

Test plan

New file sdk/python/tests/test_memory_scope_kwargs.py (all @pytest.mark.unit, mocking the low-level client the same way neighboring memory tests do):

  • scope=None default behavior unchanged for set/get/delete
  • scope="global" delegation for set/get/delete (and scope_id ignored for global)
  • scope="session"|"actor"|"workflow" delegation with explicit scope_id for set/get/delete
  • non-global scopes without scope_id delegate through the current execution context
  • invalid scope raises ValueError listing valid scopes (on set/get/delete)
  • non-global scope without scope_id raises ValueError pointing at the accessor form
  • no search happy-path test needed (Change 2 was a README fix, not a new method)

Results (run in sdk/python):

pytest tests/test_memory_scope_kwargs.py tests/test_memory_client_core.py tests/test_memory_invariants.py
49 passed

ruff check and ruff format pass on the touched files.

Follow-ups filed separately

Out of scope for this PR, noted for later:

  • Go harness Result cost field parity
  • Go memory on_change parity
  • ExecutionFilter version field

Cross-SDK memory check

  • TypeScript scalar memory set/get/delete already derived scoped headers from metadata when scopeId was omitted.
  • Go scoped memory helpers already derive workflow/session/actor IDs from ExecutionContext, and the control-plane backend intentionally omits empty scope headers without client-side failure.
  • Found and fixed one adjacent TypeScript gap in MemoryEventClient.history: event history filtering now derives scope_id from metadata when scopeId is omitted, matching the scalar memory client behavior and the control-plane history query contract.

Additional verification:

cd sdk/typescript && npm test -- --run tests/memory_client_scopes.test.ts tests/memory_client.test.ts tests/memory_interface.test.ts tests/memory_event_client.test.ts
cd sdk/typescript && npm run build
cd sdk/python && /tmp/agentfield-pr707-venv/bin/pytest tests/test_memory_scope_kwargs.py tests/test_memory_client_core.py tests/test_memory_invariants.py

…rrect README search API

Add optional scope/scope_id kwargs to app.memory.set/get/delete so the
developer-facing MemoryInterface matches what humans and LLMs naturally guess
(app.memory.set(key, data, scope="global")) and mirrors the TypeScript SDK,
which already accepts scope positionally. scope=None keeps today's hierarchical
behavior unchanged; explicit scopes route to the existing accessor clients.
Invalid scopes raise a ValueError listing valid scopes; non-global scopes
without a scope_id raise a clear ValueError.

No general memory search endpoint exists on the control plane (only
/api/v1/memory/vector/search, already exposed as similarity_search), so the
README's advertised app.memory.search(...) is corrected to similarity_search
and the scope names are corrected from 'agent/run' to 'actor/workflow'.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.4 KB +4% 0.32 µs -9%
TS 353 B - 1.65 µs -18%

✓ No regressions detected

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.00% 87.40% ↓ -0.40 pp 🟡
sdk-go 91.80% 92.00% ↓ -0.20 pp 🟢
sdk-python 93.91% 93.73% ↑ +0.18 pp 🟢
sdk-typescript 90.06% 90.42% ↓ -0.36 pp 🟢
web-ui 84.83% 84.79% ↑ +0.04 pp 🟡
aggregate 85.63% 85.75% ↓ -0.12 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 0 ➖ no changes
sdk-go 0 ➖ no changes
sdk-python 25 100.00%
sdk-typescript 3 100.00%
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Python memory scope kwargs DX

1 participant